import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;

public class HexView extends JScrollPane {
    private class LineNoListModel extends AbstractListModel<Integer> {
        private int size;
        public void setSize(int size) {
            int oldSize = this.size;
            this.size = size;
            if (oldSize > size) {
                fireIntervalRemoved(this, size, oldSize - 1);
            } else if (oldSize < size) {
                fireIntervalAdded(this, oldSize, size - 1);
            }
        }
        @Override
        public int getSize() {
            return size;
        }
        @Override
        public Integer getElementAt(int index) {
            return index + 1;
        }
    }
    private class LineNumListCellRenderer extends JLabel implements ListCellRenderer<Integer> {
        @Override
        public Component getListCellRendererComponent(JList<? extends Integer> list, Integer value, int index, boolean isSelected, boolean cellHasFocus) {
            setText(String.format("%08x", index * 0x10));
            return this;
        }
    }
    private JTextPane ta = new JTextPane();
    private Font f = FileUtil.loadFont("consolas.ttf", 18);
    private LineNoListModel listLineNoModel = new LineNoListModel();
    private JList<Integer> listLineNo = new JList<>(listLineNoModel);
    private LineNumListCellRenderer listLineNoRenderer = new LineNumListCellRenderer();
    private File file;
    private String hexStr;
    private String origText;
    private void buildHexStr(String text) {
        // 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF 0123456789ABCDEF
        StringBuilder sb = new StringBuilder();
        StringBuilder line = new StringBuilder(" ");
        for (int i = 0; i < text.length(); i++) {
            int byt = (int) (text.charAt(i) + 256) % 256;
            int hi = byt / 16, lo = byt % 16;
            if (hi < 10) hi += '0';
            else hi += 'A' - 10;
            if (lo < 10) lo += '0';
            else lo += 'A' - 10;
            line.append(new String(new char[] {(char) hi, (char) lo}));
            line.append(" ");
            if (i % 16 == 15) {
                for (int j = i / 16 * 16; j <= i; j++) {
                    int byt_ = (int) (text.charAt(j) + 256) % 256;
                    char fuckyou = (char) byt_;
                    line.append(new String(new char[] {fuckyou >= 0x20 && fuckyou <= 0x7e ? fuckyou : '.'}));
                }
                sb.append(line.toString());
                sb.append("\n");
                line = new StringBuilder(" ");
            } else if (i == text.length() - 1) {
                for (int j = i + 1; j <= i / 16 * 16 + 15; j++) {
                    line.append("   ");
                }
                for (int j = i / 16 * 16; j <= i; j++) {
                    int byt_ = (int) (text.charAt(j) + 256) % 256;
                    char fuckyou = (char) byt_;
                    line.append(new String(new char[] {fuckyou >= 0x20 && fuckyou <= 0x7e ? fuckyou : '.'}));
                }
                sb.append(line.toString());
            }
        }
        hexStr = sb.toString();
    }
    private void updateLineCount(int lines) {
        listLineNoModel.setSize(lines);
        if (lines > 0) {
            FontMetrics metrics = listLineNo.getFontMetrics(f);
            int maxNumStringLength = String.format("%08x", lines * 0x10).length();
            int width = (int) (metrics.charWidth('a') * maxNumStringLength + 0.5) + 5; // 等宽字体所有字符宽度相等
            listLineNo.setFixedCellWidth(width);
        }
    }
    public HexView(File file) {
        this.file = file;
        setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        setViewportView(ta);
        setRowHeaderView(listLineNo);
        ta.setEditable(false);
        listLineNoRenderer.setFont(f);
        listLineNo.setFixedCellWidth(38);
        listLineNo.setBackground(Color.LIGHT_GRAY);
        listLineNo.setCellRenderer(listLineNoRenderer);
        listLineNo.setFont(f);
        ta.setFont(f);
        ta.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void removeUpdate(DocumentEvent e) {
                int lines = ta.getDocument().getDefaultRootElement().getElementCount();
                updateLineCount(lines);
            }
            @Override
            public void insertUpdate(DocumentEvent e) {
                int lines = ta.getDocument().getDefaultRootElement().getElementCount();
                updateLineCount(lines);
            }
            @Override
            public void changedUpdate(DocumentEvent e) {
                int lines = ta.getDocument().getDefaultRootElement().getElementCount();
                updateLineCount(lines);
            }
        });
    }
    public String getText() {
        return origText;
    }
    public void setText(String str) {
        buildHexStr(str);
        this.origText = str;
        ta.setText(hexStr);
    }
    public File getFile() {
        return file;
    }
    public void setFile(File file) {
        this.file = file;
    }
    public Font getTextAreaFont() {
        return this.f;
    }
    public void setTextAreaFont(Font f) {
        ta.setFont(f);
        listLineNoRenderer.setFont(f);
        this.f = f;
        updateLineCount(ta.getDocument().getDefaultRootElement().getElementCount());
    }
}